﻿using log4net;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Zbus.Mq;
namespace Zbus.Rpc
{

    public class RpcProcessor
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(RpcProcessor));

        public Encoding Encoding { get; set; } = Encoding.UTF8;

        public string DocContext { get; set; } = "";

        public Dictionary<string, Dictionary<string, MethodInstance>> Modules { get; } = new Dictionary<string, Dictionary<string, MethodInstance>>();


        private Dictionary<string, MethodInstance> methods = new Dictionary<string, MethodInstance>();

         

        public void AddModule<T>(string module = null)
        {
            Type type = typeof(T);
            object instance = Activator.CreateInstance(type);
            if (module == null)
            {
                AddModule(instance);
            }
            else
            {
                AddModule(module, instance);
            }
        }

        public void AddModule<T>()
        {
            AddModule(typeof(T)); 
        }

        public void AddModule(Type t)
        { 
            object instance = t.GetConstructors()[0].Invoke(new object[0]);
            AddModule(instance);
        }

        public void AddModule(string module, Type t)
        {
            object instance = t.GetConstructors()[0].Invoke(new object[0]);
            AddModule(module, instance);
        }

        public void AddModule(object service)
        {
            foreach (Type type in service.GetType().GetInterfaces())
            {
                AddModule(type.Name, service);
                AddModule(type.FullName, service);
            }

            AddModule("", service);
            AddModule(service.GetType().Name, service);
            AddModule(service.GetType().FullName, service);
        }

        public void AddModule(string module, object service)
        {
            BuildMethodTable(methods, module, service); 
        } 

        public void EnableDoc(bool value=true)
        {
            if (!value) return;

            string key = "index:Index";
            if (!this.methods.ContainsKey(key))
            {
                this.AddModule("index", new RpcModuleInfo(this));
            }
            
            foreach (var kv in this.Modules)
            {
                string moduleName = kv.Key;
                string moduleIndexName = moduleName + ":Index";
                if (!this.methods.ContainsKey(moduleIndexName))
                {
                    var moduleInfo = new RpcModuleInfo(this, moduleName);
                    MethodInfo info = moduleInfo.GetType().GetMethod("Index");
                    MethodInstance mi = new MethodInstance(info, moduleInfo);
                    this.methods[moduleIndexName] = mi;
                }
            }
        }

        private void BuildMethodTable(IDictionary<string, MethodInstance> table, string module, object service)
        {
            if (!Modules.ContainsKey(module))
            {
                Modules[module] = new Dictionary<string, MethodInstance>();
            }
            Dictionary<string, MethodInstance> moduleInfo = Modules[module];


            IDictionary<string, MethodInstance> ignore = new Dictionary<string, MethodInstance>();
            List<Type> types = new List<Type>();
            types.Add(service.GetType());
            foreach (Type type in service.GetType().GetInterfaces())
            {
                types.Add(type);
            }
            foreach (Type type in types)
            {
                foreach (MethodInfo info in type.GetMethods())
                {
                    bool exclude = false;
                    string id = info.Name;
                    if (info.DeclaringType != type || !info.IsPublic) continue;

                    foreach (Attribute attr in Attribute.GetCustomAttributes(info))
                    {
                        if (attr.GetType() == typeof(Remote))
                        {
                            Remote r = (Remote)attr;
                            if (r.Id != null)
                            {
                                id = r.Id;
                            }
                            if (r.Exclude)
                            {
                                exclude = true;
                            }
                            break;
                        }
                    }
                    ParameterInfo[] paramInfo = info.GetParameters();
                    Type[] paramTypes = new Type[paramInfo.Length];
                    for(int i = 0; i < paramTypes.Length; i++)
                    {
                        paramTypes[i] = paramInfo[i].ParameterType;
                    }
                    IList<string> keys = Keys(module, id, paramTypes);

                    MethodInstance instance = new MethodInstance(info, service);
                    foreach(string key in keys)
                    {
                        table[key] = instance;
                    } 
                    if (exclude)
                    {
                        foreach (string key in keys)
                        {
                            ignore[key] = instance;
                        }
                    }
                    else
                    {
                        moduleInfo[id] = instance;
                    } 
                }
            }
            foreach (string key in ignore.Keys)
            {
                table.Remove(key);
            }  

        } 
        
         
        public void MessageHandler(Message msg, MqClient client)
        {
            Message msgRes = new Message
            { 
                Recver = msg.Sender,
                Id = msg.Id,
                Topic = msg.Topic,
            };
            int status = 600;
            dynamic result = null; 
            try
            {
                string encodingName = msg.Encoding;
                Encoding encoding = this.Encoding;
                if (encodingName != null)
                {
                    encoding = Encoding.GetEncoding(encodingName);
                }

                Request request = JsonKit.DeserializeObject<Request>(msg.GetBody(encoding));

                result = ProcessAsync(request, msg).Result;
                if(!(result is Message))
                {
                    status = 200;
                }  
            }
            catch (Exception e)
            {
                if(e is AggregateException)
                {
                    e = e.InnerException;
                }
                result = e;
            } 

            try
            {
                if (result is Message)
                {
                    Message httpMsg = (Message)result;
                    httpMsg.Id = msgRes.Id;
                    httpMsg.Recver = msgRes.Recver;
                    httpMsg.Topic = msgRes.Topic;
                    msgRes = httpMsg;
                    if(msgRes.Status == null)
                    {
                        msgRes.Status = 200;
                    }
                }
                else
                {
                    msgRes.Status = status;
                    msgRes.SetJsonBody(JsonKit.SerializeObject(result), this.Encoding);
                }  
                Task task = client.RouteAsync(msgRes);
            }
            catch (Exception e)
            {
                log.Error(e);
            }
        }

        private async Task<dynamic> ProcessAsync(Request request, Message msg)
        { 
            string module = request.Module;
            string method = request.Method;
            object[] args = request.Params;
            if (module == null || module.Length == 0) module = "index";
            if (method == null || method.Length == 0) method = "index";
            if (args == null) args = new object[0];

            MethodInstance target = null;
            if (method == null)
            {
                throw new RpcException("missing method name"); 
            }

            target = FindMethod(module, method, args);
            if (target == null)
            {
                string errorMsg = method + " not found";
                if (module != "")
                {
                    errorMsg = module + ":" + errorMsg;
                }
                throw new RpcException(errorMsg); 
            } 

            try
            {
                ParameterInfo[] pinfo = target.Method.GetParameters();
                CheckParams(module, target.Method, pinfo, args);
                int j = 0;
                object[] invokeArgs = new object[pinfo.Length];
                for (int i = 0; i < pinfo.Length; i++)
                { 
                    if(pinfo[i].ParameterType == typeof(Message))
                    {
                        invokeArgs[i] = msg;
                        continue;
                    }
                    if (args[j].GetType() != pinfo[i].ParameterType)
                    {
                        invokeArgs[i] = JsonKit.Convert(args[j], pinfo[i].ParameterType);
                    } 
                    else
                    {
                        invokeArgs[i] = args[j];
                    }
                    j++;
                }

                dynamic invoked = target.Method.Invoke(target.Instance, invokeArgs);
                if (invoked != null && typeof(Task).IsAssignableFrom(invoked.GetType()))
                {
                    if (target.Method.ReturnType.GenericTypeArguments.Length > 0)
                    {
                        return await invoked;
                    }
                    else
                    {
                        return null;
                    }
                }
                else
                {
                    return invoked;
                } 
            }
            catch (Exception ex)
            { 
                if (ex.InnerException != null)
                {
                    throw ex.InnerException;
                }
                throw ex;
            }
        }

        private void CheckParams(string module, MethodInfo method, ParameterInfo[] pinfo, object[] args)
        {
            int requiredArgsCount = 0;
            foreach(ParameterInfo info in pinfo) {
                if (info.ParameterType == typeof(Message)) continue;
                requiredArgsCount++;
            }
            if(requiredArgsCount != args.Length)
            {
                string requiredParamTypeString = "";
                for (int i = 0; i < pinfo.Length; i++)
                {
                    requiredParamTypeString += pinfo[i].ParameterType.Name;
                    if (i < pinfo.Length - 1)
                    {
                        requiredParamTypeString += ", ";
                    }
                } 
                String gotParamsString = "";
                for (int i = 0; i <args.Length; i++){
                    gotParamsString += args[i];
				    if(i<args.Length-1){
					    gotParamsString += ", ";
				    }
                }
                string errorMsg = string.Format("{0}:{1}({2}), called with {1}({3})",
                       module, method.Name, requiredParamTypeString, gotParamsString);
                throw new ArgumentException(errorMsg);
            }
        }

        private MethodInstance FindMethod(string module, string method, object[] args)
        {
            Type[] types = null;
            if (args != null)
            {
                types = new Type[args.Length];
                for (int i = 0; i < args.Length; i++)
                {
                    types[i] = args[i].GetType();
                }
            }

            IList<string> keys = Keys(module, method, types);
            foreach (string key in keys)
            {
                if (this.methods.ContainsKey(key))
                {
                    return this.methods[key];
                }
            }
            return null;
        } 

        private IList<string> Keys(string module, string method, Type[] types)
        {
            string paramMD5 = null, key;
            if(types != null)
            {
                foreach (Type type in types)
                {
                    paramMD5 += type + ",";
                } 
            } 

            IList<string> keys = new List<string>();
            key = module + ":" + method;
            if (paramMD5 != null)
            {
                key += ":" + paramMD5;
            }

            if (!keys.Contains(key))
            {
                keys.Add(key);
            }

            key = module + ":" + char.ToUpper(method[0]) + method.Substring(1);
            if (!keys.Contains(key))
            {
                keys.Add(key);
            }

            key = module + ":" + char.ToLower(method[0]) + method.Substring(1);
            if (!keys.Contains(key))
            {
                keys.Add(key);
            }

            key = module + ":" + char.ToUpper(method[0]) + method.Substring(1);
            if (paramMD5 != null)
            {
                key += ":" + paramMD5;
            }
            if (!keys.Contains(key))
            {
                keys.Add(key);
            }

            key = module + ":" + char.ToLower(method[0]) + method.Substring(1);
            if (paramMD5 != null)
            {
                key += ":" + paramMD5;
            }
            if (!keys.Contains(key))
            {
                keys.Add(key);
            }

            string async = "Async";
            if (method.EndsWith(async)) //special for Async method
            {
                key = module + ":" + method.Substring(0, method.Length - async.Length);
                if (paramMD5 != null)
                {
                    key += ":" + paramMD5;
                }
                if (!keys.Contains(key))
                {
                    keys.Add(key);
                }
            }
            return keys;
        }

        public class MethodInstance
        {
            public MethodInfo Method { get; set; }
            public object Instance { get; set; }

            public MethodInstance(MethodInfo method, object instance)
            {
                this.Method = method;
                this.Instance = instance;
            }
        }
    }

    public class Remote : Attribute
    {
        public string Id { get; set; }
        public bool Exclude { get; set; }

        public Remote()
        {
            Id = null;
        }

        public Remote(string id)
        {
            this.Id = id;
        }

        public Remote(bool exclude)
        {
            this.Exclude = exclude;
        }
    }


    public class RpcModuleInfo
    {
        private RpcProcessor rpcProcessor;
        private string module;
        public RpcModuleInfo(RpcProcessor rpcProcessor, string module = null)
        {
            this.rpcProcessor = rpcProcessor;
            this.module = module;
        }

        public Message Index()
        {
            Message res = new Message();
            res.Status = 200;
            res.Headers["content-type"] = "text/html"; 
            res.BodyString = BuildRpcInfo(this.module);

            return res;
        }

        private string BuildModuleInfo(string module)
        {
            var moduleInfo = "";
            var topic = this.rpcProcessor.DocContext;
            var modules = this.rpcProcessor.Modules;
            if (module != null && modules.ContainsKey(module))
            {
                var moduleMethods = this.rpcProcessor.Modules[module];
                foreach (var kv in moduleMethods)
                {
                    var methodName = kv.Key;
                    var m = moduleMethods[methodName];
                    string returnType = m.Method.ReturnType.ToString();
                    var args = "";
                    ParameterInfo[] pinfos = m.Method.GetParameters();
                    for(int i=0; i < pinfos.Length; i++)
                    {
                        ParameterInfo pinfo = pinfos[i];
                        args += pinfo.ToString();
                        if (i < pinfos.Length - 1)
                        {
                            args += ", ";
                        }
                    } 
                    moduleInfo += string.Format(RpcModuleTemplate, topic, module, methodName, args, returnType);
                } 
            }
            return moduleInfo;
        }

        private string BuildRpcInfo(string module = null)
        {
            string modulesInfo = "";
            var modules = this.rpcProcessor.Modules;
            if (module == null)
            {
                foreach(var kv in modules)
                {
                    modulesInfo += BuildModuleInfo(kv.Key);
                }
            }
            else
            {
                modulesInfo = BuildModuleInfo(module);
            }
            return string.Format(RpcInfoTemplate, rpcProcessor.DocContext, RpcStyleTemplate, modulesInfo); 
        }


        public static readonly string RpcInfoTemplate = @"
<html><head>
<meta http-equiv=""Content-type"" content=""text/html; charset=utf-8"">
<title>{0} C#</title>
{1}
</head>
<body>
<div>
<div class=""url"">
    <span>URL=/{0}/[module]/[method]/[param1]/[param2]/...</span>
    <a href=""/"">zbus</a>
    <a href=""/{0}"">service home</a>
</div>
<table class=""table"">
<thead>
<tr class=""table-info"">
    <th class=""returnType"">Return Type</th>
    <th class=""methodParams"">Method and Params</th>
    <th class=""modules"">Module</th>
</tr>
<thead>
<tbody>
{2}
</tbody>
</table> </div> </body></html>
";


        public static readonly string RpcStyleTemplate = @"
<style type=""text/css"">
body {
    font-family: -apple-system,system-ui,BlinkMacSystemFont,""Segoe UI"",Roboto,""Helvetica Neue"",Arial,sans-serif;
    font-size: 1rem;
    font-weight: 400;
    line-height: 1.5;
    color: #292b2c;
    background-color: #fff;
    margin: 0px;
    padding: 0px;
}
table {  background-color: transparent;  display: table; border-collapse: separate;  border-color: grey; }
.table { width: 100%; max-width: 100%;  margin-bottom: 1rem; }
.table th {  height: 30px; }
.table td, .table th {    border-bottom: 1px solid #eceeef;   text-align: left; }
th.returnType {  width: 20%; }
th.methodParams {   width: 40%; }
th.modules {  width: 40%; }
thead { display: table-header-group; vertical-align: middle; border-color: inherit;}
tbody { display: table-row-group; vertical-align: middle; border-color: inherit;}
tr { display: table-row;  vertical-align: inherit; border-color: inherit; }
.table-info, .table-info>td, .table-info>th { background-color: #dff0d8; }
.url { margin: 4px 0; }
</style>
";

        public static readonly string RpcModuleTemplate = @"
<tr>
    <td class=""returnType"">{4}</td>
    <td class=""methodParams"">
        <code><strong><a href=""/{0}/{1}/{2}""/ target=""_blank"">{2}</a></strong>({3})</code>
    </td>
    <td class=""modules""> <a href=""/{0}/{1}"">{1}</a>  </td>
</tr>
";
    }


}